Java 和 Native 通信

在 Android 开发过程中,有时为了提升算法效率,往往采用 JNI 的方式调用 C/C++ 程序,这里主要总结了 Java 调用 Native 和 Native 调用 Java 的交互方式和实现原理。

Java 调用 Native

加载动态库

Android 虚拟机为每个进程分配一个 JavaVM 实例,在 Java 层中通过如下方式加载动态库:

1
2
3
static{
loadLibrary("classLoad");
}

在 JNI 中以下函数会被调用:

1
2
3
4
5
6
7
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv *env;
if(JNI_OK != (*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4 ) )
{
return JNI_ERR;
}
}

这里可以直接获取 JavaVM 的实例 vm,然后调用 GetEnv() 获取当前线程对应的 JNI 指针 JNIEnv。JNIEnv 是一个与线程相关的,代表 JNI 环境的结构体,可以通过它调用 Java 方法和操作 jobject 对象(jobject 对象代表的是传入 JNI 层的 Java 对象)。

动态注册本地函数

在 JNI_OnLoad 中会进行初始化工作,比如,先查找对应路径下的类对象,再向 JVM 注册类对象中使用的本地方法,以此形成一个映射表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv *env;
if(JNI_OK != (*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4 ) )
{
return JNI_ERR;
}
jclass clazz = (*env)->FindClass(env, classPathName);
if (clazz == NULL) {
return JNI_ERR;
}
assert(env != NULL);
if ((*env)->RegisterNatives(env, clazz, g_NativeMethods, 1) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_4;
}

static const JNINativeMethod g_NativeMethods[] = {
// private native void nativeGet(long director, String path);
{ "nativeGet",
"(JLjava/lang/String;)V",
(void*)nativeGet
},
}

调用 JNI 提供的 RegisterNatives 函数,将本地的函数映射表注册到 JVM 中,这样 JVM 就可以根据函数映射表来调用相应的本地函数。

我们知道 Java 调用 JNI 还有一种方式,就是按照 JNI 规范的命名规则定义 JNI 函数:

1
JNIEXPORT JNI返回类型 JNICALL java_完整包名_类名_方法名();

这种静态注册的方式没有实现 JNI_OnLoad() 函数,初次调用时需要根据函数名在 JNI 层中搜索对应的本地函数,然后调用 ResolveNativeMethod 建立对应关系,这个过程比较耗时,因此程序运行效率相对动态注册而言较低。

Native 调用 Java

寻找类对象

调用 FindClass 函数,传入一个 Class 描述符(Engine 类的描述符是 com/example/Engine),JVM 会从 classpath 路径下搜索该类,并返回 jclass 类型。

1
2
3
4
jclass clazz = (*env)->FindClass(env, classPathName);
if (clazz == NULL) {
return JNI_ERR;
}

获取方法 ID

使用GetMethodID 获取类的方法 ID,返回类型是 jmethodID,静态方法需要使用GetStaticMethodID 获取。因为 JVM 会为每个注册的 Java 方法设定一个 ID,所以这里获取方法 ID 后可以进一步获取对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jmethodID g_getPath = getStaticMethodIDCheck(env, g_Engine_class, "getPathStatic", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); 

jmethodID getStaticMethodIDCheck(JNIEnv* env, jclass clazz,
const char methodname[], const char type[] )
{
if(NULL == clazz){
return NULL;
}
jmethodID id = (*env)->GetStaticMethodID( env, clazz, methodname, type );
if(NULL == id){
LOG("register %s method failed", methodname);
}
return id;
}

调用对象方法

传入类对象,对象方法的 jmethodID,通过 callMethod 或 callStaticMethod 调用 ID 对应的 Java 方法,JVM 针对所有数据类型的返回值都定义了相关的函数。例如调用类的静态方法,CallStaticIntMethod、CallStaticFloatMethod、CallStaticObjectMethod 分别对应调用返回是 int,float,Object 类型的方法,对于返回的类型不是基本类型,而是指向对象的引用类型时,统一使用 CallStaticObjectMethod 调用。

1
2
3
4
5
JNIEnv* env;
if((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK){
return;
}
jstring jsPath = (*env)->CallStaticObjectMethod(env, g_Engine_class, g_getPath, jsName, jsType);

如上所述,JNIEnv 是与线程相关的,使用它调用 Java 方法和操作 jobject 对象时需要先使用 JVM 的实例绑定当前线程。然后就可以调用到 Java 层 Engine 类的 getPathStatic 方法。

知道是不会有人点的,但万一被感动了呢?